package map;

import java.awt.Image;
import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import java.util.Observer;

public class Copy_2_of_Map extends Observable implements Observer
{
    protected MapData data;
    protected MapLibrary library;
    
    public Copy_2_of_Map(MapData data, MapLibrary library)
    {
        this.data = data;
        this.library = library;
        data.addObserver(this);
    }
    
    public void setMapData(MapData data)
    {
        this.data.deleteObserver(this);
        this.data = data;
        data.addObserver(this);
    }
    
    /** Gets a properly rotated Image corresponding to the terrain
     * tile at the specified location
     * @param x         X coordinate of terrain
     * @param y         Y coordinate of terrain
     * @return A rotated Image corresponding to the terrain tile*/
    public Image getTerrainImage(int x, int y)
    {
        if(data.terrain[x][y] == null || library.getTerrainEntry(data.terrain[x][y].getType()) == null)
            return null;
        return library.getTerrainEntry(data.terrain[x][y].getType()).getImage(data.terrain[x][y].getRotation());
    }

    /** Gets a properly rotated Image corresponding to the item
     * at the specified location
     * @param x         X coordinate of item
     * @param y         Y coordinate of item
     * @return A rotated Image corresponding to the item*/
    public Image getItemImage(int x, int y)
    {
        MapDataItemInstance item = data.getItem(x, y);
        if(item == null)
            return null;
        return library.getItemEntry(item.getType()).getImage(item.getRotation());
    }
    
    /** Gets the MapLibrary currently used by this MapData
     * @return This MapData's MapLibrary*/
    public MapLibrary getLibrary()
    {
        return library;
    }
    
    
    
    
    
    
    
    
    
    
    
    
    /* RANDOM FILL */
    
    public void randomFill()
    {
        // The algorithm operates by placing new terrain next to existing
        // terrain. If map is completely empty to begin with, add one random tile to the
        // middle.
        if(data.hasNoTerrain())
        {
            // Place a random tile
            ArrayList<MapLibraryTerrainEntry> terrainEntrys = (ArrayList<MapLibraryTerrainEntry>)library.getTerrainList();
            //setTerrain((int)(Math.random() * terrain.length), (int)(Math.random() * terrain[0].length),
            //      terrainEntrys.get((int)(Math.random() * terrainEntrys.size())).getID(),
            //      (int)(Math.random() * 4));
            data.setTerrain(0, 0,
                    terrainEntrys.get((int)(Math.random() * terrainEntrys.size())).getID(),
                    (int)(Math.random() * 4));
        }
        while(!isFull(data.terrain))
        {
            int[] xANDy = getNonEmptySquareNextToEmptySquare();
            int x = xANDy[0];
            int y = xANDy[1];
            data.setTerrain(randomFillHelper(data.terrain, x, y).terrain);
        }
    }

    // Fills map with tiles until is blocked in
    private RecursiveFillInfo randomFillHelper(MapDataTerrainInstance[][] terrain, int x, int y)
    {
        MapDataTerrainInstance[][] current = copyTerrainArray(terrain);
        ArrayList<Integer> emptyNeighbors = getEmptyNeighborDirections(current, x, y);
        while(emptyNeighbors.size() > 0)
        {
            //Integer direction = emptyNeighbors.remove((int)(Math.random() * emptyNeighbors.size()));
            Integer direction = emptyNeighbors.remove(0);
            int nx = x, ny = y;
            switch(direction)
            {
                case 0: ny--; break;
                case 1: nx++; break;
                case 2: ny++; break;
                default: nx--; break;
            }
            if(hasNoTerrainAt(current, nx, ny))
            {
                ArrayList<MapLibraryTerrainEntry> potentialTiles = 
                    (ArrayList<MapLibraryTerrainEntry>)library.
                    getPotentialAdjacentTiles(getEdges(current[x][y], direction));
                search: while(true)
                {
                    if(potentialTiles.size() == 0)
                        return new RecursiveFillInfo(false, null); //This configuration is illegal
                    MapLibraryTerrainEntry entry = potentialTiles.
                        remove((int)(potentialTiles.size() * Math.random()));
                    int finalRotation = (int)(Math.random() * 4);
                    int rotation = finalRotation;
                    do
                    {
                        rotation = (rotation + 1) % 4;
                        current[nx][ny] = new MapDataTerrainInstance(entry.getID(), rotation);
                        if(terrainTileIsLegal(current, nx, ny))
                        {
                            RecursiveFillInfo temp = randomFillHelper(current, nx, ny);
                            if(temp.wasSuccessful)
                            {
                                current = temp.terrain;
                                break search;
                            }
                        }
                    }while(rotation != finalRotation);
                }
            }
        }
        return new RecursiveFillInfo(true, current);
    }
    
    private boolean terrainTileIsLegal(MapDataTerrainInstance[][] current, int nx, int ny)
    {
        int currentID = current[nx][ny].getType();
        int currentRotation = current[nx][ny].getRotation();
        MapLibraryTerrainEntry currentEntry = library.getTerrainEntry(currentID);
        if(hasTerrainAt(current, nx, ny - 1))
        {
            int northID = current[nx][ny - 1].getType();
            int northRotation = current[nx][ny - 1].getRotation();
            MapLibraryTerrainEntry northEntry = library.getTerrainEntry(northID);
            if(!MapLibrary.edgesAreCompatible(northEntry.getSouthEdges(northRotation),
                    currentEntry.getNorthEdges(currentRotation)))
                return false;
        }
        if(hasTerrainAt(current, nx, ny + 1))
        {
            int southID = current[nx][ny + 1].getType();
            int southRotation = current[nx][ny + 1].getRotation();
            MapLibraryTerrainEntry southEntry = library.getTerrainEntry(southID);
            if(!MapLibrary.edgesAreCompatible(southEntry.getNorthEdges(southRotation),
                    currentEntry.getSouthEdges(currentRotation)))
                return false;
        }
        if(hasTerrainAt(current, nx - 1, ny))
        {
            int westID = current[nx - 1][ny].getType();
            int westRotation = current[nx - 1][ny].getRotation();
            MapLibraryTerrainEntry westEntry = library.getTerrainEntry(westID);
            if(!MapLibrary.edgesAreCompatible(westEntry.getEastEdges(westRotation),
                    currentEntry.getWestEdges(currentRotation)))
                return false;
        }
        if(hasTerrainAt(current, nx + 1, ny))
        {
            int eastID = current[nx + 1][ny].getType();
            int eastRotation = current[nx + 1][ny].getRotation();
            MapLibraryTerrainEntry eastEntry = library.getTerrainEntry(eastID);
            if(!MapLibrary.edgesAreCompatible(eastEntry.getWestEdges(eastRotation),
                    currentEntry.getEastEdges(currentRotation)))
                return false;
        }
        return true;
    }
    
    private static boolean isFull(MapDataTerrainInstance[][] terrain)
    {
        for(int x = 0; x < terrain.length; x++)
        {
            for(int y = 0; y < terrain[x].length; y++)
            {
                if(terrain[x][y] == null)
                    return false;
            }
        }
        return true;
    }

    private MapDataTerrainInstance[][] copyTerrainArray(MapDataTerrainInstance[][] terrain)
    {
        MapDataTerrainInstance[][] newTerrain =
            new MapDataTerrainInstance[terrain.length][terrain[0].length];
        for(int x = 0; x < terrain.length; x++)
        {
            for(int y = 0; y < terrain[x].length; y++)
            {
                newTerrain[x][y] = terrain[x][y];
            }
        }
        return newTerrain;
    }
    
    private int[] getNonEmptySquareNextToEmptySquare()
    {
        for(int x = 0; x < data.terrain.length; x++)
        {
            for(int y = 0; y < data.terrain[x].length; y++)
            {
                if(data.hasTerrainAt(x, y) &&
                    (
                            (data.hasNoTerrainAt(x-1, y)) ||
                            (data.hasNoTerrainAt(x+1, y)) ||
                            (data.hasNoTerrainAt(x, y-1)) ||
                            (data.hasNoTerrainAt(x, y+1))
                    ))
                    return new int[]{x, y};
            }
        }
        return new int[]{-1, -1};
    }
    
    private class RecursiveFillInfo
    {
        boolean wasSuccessful;
        MapDataTerrainInstance[][] terrain;
        
        public RecursiveFillInfo(boolean wasSuccessful, MapDataTerrainInstance[][] terrain)
        {
            this.wasSuccessful = wasSuccessful;
            this.terrain = terrain;
        }
    }
    
    private ArrayList<Integer> getEmptyNeighborDirections(MapDataTerrainInstance[][] terrain, int x, int y)
    {
        ArrayList<Integer> directions = new ArrayList<Integer>();
        if(hasNoTerrainAt(terrain, x-1, y)) directions.add(3);
        if(hasNoTerrainAt(terrain, x+1, y)) directions.add(1);
        if(hasNoTerrainAt(terrain, x, y-1)) directions.add(0);
        if(hasNoTerrainAt(terrain, x, y+1)) directions.add(2);
        return directions;
    }

    /** Tells if location has no terrain tile
     * This method is not the logical negation of the method hasTerrainAt
     * because both methods return false if coordinates
     * are out of bounds
     * @param x         X coordinate of query location
     * @param y         Y coordinate of query location
     * @return True if location has an terrain; false otherwise*/
    private boolean hasNoTerrainAt(MapDataTerrainInstance[][] terrain, int x, int y)
    {
        return x >= 0 && y >= 0 &&
            x < terrain.length &&
            y < terrain[0].length &&
            terrain[x][y] == null;
    }

    /** Tells if location has a terrain tile
     * This method is not the logical negation of the method hasNoTerrainAt
     * because both methods return false if coordinates
     * are out of bounds
     * @param x         X coordinate of query location
     * @param y         Y coordinate of query location
     * @return True if location has an terrain; false otherwise*/
    private boolean hasTerrainAt(MapDataTerrainInstance[][] terrain, int x, int y)
    {
        return x >= 0 && y >= 0 &&
            x < terrain.length &&
            y < terrain[0].length &&
            terrain[x][y] != null;
    }
    
    private List<String> getEdges(MapDataTerrainInstance terrain, int direction)
    {
        switch(direction)
        {
            case 0: return library.getTerrainEntry(terrain.getType()).getNorthEdges(terrain.getRotation());
            case 1: return library.getTerrainEntry(terrain.getType()).getEastEdges(terrain.getRotation());
            case 2: return library.getTerrainEntry(terrain.getType()).getSouthEdges(terrain.getRotation());
            default: return library.getTerrainEntry(terrain.getType()).getWestEdges(terrain.getRotation());
        }
    }

    @Override
    public void update(Observable arg0, Object arg1)
    {
        this.setChanged();
        this.notifyObservers(arg1);
    }

    public void requestRepaint()
    {
        setChanged();
        notifyObservers(MapDisplayPanel.class);
    }
}
